// github.com/shawn0326/three.path
import * as THREE from 'three';

/**
 * PathPoint
 */
class PathPoint {
  constructor() {
    this.pos = new THREE.Vector3();
    this.dir = new THREE.Vector3();
    this.right = new THREE.Vector3();
    this.up = new THREE.Vector3(); // normal
    this.dist = 0; // distance from start
    this.widthScale = 1; // for corner
    this.sharp = false; // marks as sharp corner
  }

  lerpPathPoints(p1, p2, alpha) {
    this.pos.lerpVectors(p1.pos, p2.pos, alpha);
    this.dir.lerpVectors(p1.dir, p2.dir, alpha);
    this.up.lerpVectors(p1.up, p2.up, alpha);
    this.right.lerpVectors(p1.right, p2.right, alpha);
    this.dist = (p2.dist - p1.dist) * alpha + p1.dist;
    this.widthScale = (p2.widthScale - p1.widthScale) * alpha + p1.widthScale;
  }

  copy(source) {
    this.pos.copy(source.pos);
    this.dir.copy(source.dir);
    this.up.copy(source.up);
    this.right.copy(source.right);
    this.dist = source.dist;
    this.widthScale = source.widthScale;
  }
}

// eslint-disable-next-line camelcase
const helpVec3_1 = new THREE.Vector3();
// eslint-disable-next-line camelcase
const helpVec3_2 = new THREE.Vector3();
// eslint-disable-next-line camelcase
const helpVec3_3 = new THREE.Vector3();
const helpMat4 = new THREE.Matrix4();
const helpCurve = new THREE.QuadraticBezierCurve3();

function _getCornerBezierCurve(
  last,
  current,
  next,
  cornerRadius,
  firstCorner,
  out
) {
  const lastDir = helpVec3_1.subVectors(current, last);
  const nextDir = helpVec3_2.subVectors(next, current);

  const lastDirLength = lastDir.length();
  const nextDirLength = nextDir.length();

  lastDir.normalize();
  nextDir.normalize();

  // cornerRadius can not bigger then lineDistance / 2, auto fix this
  const v0Dist = Math.min(
    (firstCorner ? lastDirLength / 2 : lastDirLength) * 0.999999,
    cornerRadius
  );
  out.v0.copy(current).sub(lastDir.multiplyScalar(v0Dist));

  out.v1.copy(current);

  const v2Dist = Math.min((nextDirLength / 2) * 0.999999, cornerRadius);
  out.v2.copy(current).add(nextDir.multiplyScalar(v2Dist));

  return out;
}

/**
 * PathPointList
 * input points to generate a PathPoint list
 */
class PathPointList {
  constructor() {
    this.array = []; // path point array
    this.count = 0;
  }

  /**
   * Set points
   * @param {THREE.Vector3[]} points key points array
   * @param {number} cornerRadius? the corner radius. set 0 to disable round corner. default is 0.1
   * @param {number} cornerSplit? the corner split. default is 10.
   * @param {number} up? force up. default is auto up (calculate by tangent).
   * @param {boolean} close? close path. default is false.
   */
  set(points, cornerRadius = 0.1, cornerSplit = 10, up = null, close = false) {
    points = points.slice(0);

    if (points.length < 2) {
      console.warn('PathPointList: points length less than 2.');
      this.count = 0;
      return;
    }

    // Auto close
    if (close && !points[0].equals(points[points.length - 1])) {
      points.push(new THREE.Vector3().copy(points[0]));
    }

    // Generate path point list
    for (let i = 0, l = points.length; i < l; i++) {
      if (i === 0) {
        this._start(points[i], points[i + 1], up);
      } else if (i === l - 1) {
        if (close) {
          // Connect end point and start point
          this._corner(points[i], points[1], cornerRadius, cornerSplit, up);

          // Fix start point
          const dist = this.array[0].dist; // should not copy dist
          this.array[0].copy(this.array[this.count - 1]);
          this.array[0].dist = dist;
        } else {
          this._end(points[i]);
        }
      } else {
        this._corner(points[i], points[i + 1], cornerRadius, cornerSplit, up);
      }
    }
  }

  /**
   * Get distance of this path
   * @return {number}
   */
  distance() {
    if (this.count > 0) {
      return this.array[this.count - 1].dist;
    }
    return 0;
  }

  _getByIndex(index) {
    if (!this.array[index]) {
      this.array[index] = new PathPoint();
    }
    return this.array[index];
  }

  _start(current, next, up) {
    this.count = 0;

    const point = this._getByIndex(this.count);

    point.pos.copy(current);
    point.dir.subVectors(next, current);

    // init start up dir
    if (up) {
      point.up.copy(up);
    } else {
      // select an initial normal vector perpendicular to the first tangent vector
      let min = Number.MAX_VALUE;
      const tx = Math.abs(point.dir.x);
      const ty = Math.abs(point.dir.y);
      const tz = Math.abs(point.dir.z);
      if (tx < min) {
        min = tx;
        point.up.set(1, 0, 0);
      }
      if (ty < min) {
        min = ty;
        point.up.set(0, 1, 0);
      }
      if (tz < min) {
        point.up.set(0, 0, 1);
      }
    }

    point.right.crossVectors(point.dir, point.up).normalize();
    point.up.crossVectors(point.right, point.dir).normalize();
    point.dist = 0;
    point.widthScale = 1;
    point.sharp = false;

    point.dir.normalize();

    this.count++;
  }

  _end(current) {
    const lastPoint = this.array[this.count - 1];
    const point = this._getByIndex(this.count);

    point.pos.copy(current);
    point.dir.subVectors(current, lastPoint.pos);
    const dist = point.dir.length();
    point.dir.normalize();

    point.up.copy(lastPoint.up); // copy last up

    const vec = helpVec3_1.crossVectors(lastPoint.dir, point.dir);
    if (vec.length() > Number.EPSILON) {
      vec.normalize();
      const theta = Math.acos(
        Math.min(Math.max(lastPoint.dir.dot(point.dir), -1), 1)
      ); // clamp for floating pt errors
      point.up.applyMatrix4(helpMat4.makeRotationAxis(vec, theta));
    }

    point.right.crossVectors(point.dir, point.up).normalize();

    point.dist = lastPoint.dist + dist;
    point.widthScale = 1;
    point.sharp = false;

    this.count++;
  }

  _corner(current, next, cornerRadius, cornerSplit, up) {
    if (cornerRadius > 0 && cornerSplit > 0) {
      const lastPoint = this.array[this.count - 1];
      const curve = _getCornerBezierCurve(
        lastPoint.pos,
        current,
        next,
        cornerRadius,
        this.count - 1 === 0,
        helpCurve
      );
      const samplerPoints = curve.getPoints(cornerSplit); // TODO optimize

      for (let f = 0; f < cornerSplit; f++) {
        this._sharpCorner(
          samplerPoints[f],
          samplerPoints[f + 1],
          up,
          f === 0 ? 1 : 0
        );
      }

      if (!samplerPoints[cornerSplit].equals(next)) {
        this._sharpCorner(samplerPoints[cornerSplit], next, up, 2);
      }
    } else {
      this._sharpCorner(current, next, up, 0, true);
    }
  }

  // dirType: 0 - use middle dir / 1 - use last dir / 2- use next dir
  _sharpCorner(current, next, up, dirType = 0, sharp = false) {
    const lastPoint = this.array[this.count - 1];
    const point = this._getByIndex(this.count);

    const lastDir = helpVec3_1.subVectors(current, lastPoint.pos);
    const nextDir = helpVec3_2.subVectors(next, current);

    const lastDirLength = lastDir.length();

    lastDir.normalize();
    nextDir.normalize();

    point.pos.copy(current);

    if (dirType === 1) {
      point.dir.copy(lastDir);
    } else if (dirType === 2) {
      point.dir.copy(nextDir);
    } else {
      point.dir.addVectors(lastDir, nextDir);
      point.dir.normalize();
    }

    if (up) {
      if (point.dir.dot(up) === 1) {
        point.right.crossVectors(nextDir, up).normalize();
      } else {
        point.right.crossVectors(point.dir, up).normalize();
      }

      point.up.crossVectors(point.right, point.dir).normalize();
    } else {
      point.up.copy(lastPoint.up);

      const vec = helpVec3_3.crossVectors(lastPoint.dir, point.dir);
      if (vec.length() > Number.EPSILON) {
        vec.normalize();
        const theta = Math.acos(
          Math.min(Math.max(lastPoint.dir.dot(point.dir), -1), 1)
        ); // clamp for floating pt errors
        point.up.applyMatrix4(helpMat4.makeRotationAxis(vec, theta));
      }

      point.right.crossVectors(point.dir, point.up).normalize();
    }

    point.dist = lastPoint.dist + lastDirLength;

    const _cos = lastDir.dot(nextDir);
    point.widthScale = Math.min(1 / Math.sqrt((1 + _cos) / 2), 1.415) || 1;
    point.sharp = Math.abs(_cos - 1) > 0.05 && sharp;

    this.count++;
  }
}

/**
 * PathGeometry
 */
class PathGeometry extends THREE.BufferGeometry {
  /**
   * @param {Object|Number} initData - If initData is number, geometry init by empty data and set it as the max vertex. If initData is Object, it contains pathPointList and options.
   * @param {Boolean} [generateUv2=false]
   */
  constructor(initData = 3000, generateUv2 = false) {
    super();

    if (isNaN(initData)) {
      this._initByData(
        initData.pathPointList,
        initData.options,
        initData.usage,
        generateUv2
      );
    } else {
      this._initByMaxVertex(initData, generateUv2);
    }
  }

  _initByMaxVertex(maxVertex, generateUv2) {
    this.setAttribute(
      'position',
      new THREE.BufferAttribute(new Float32Array(maxVertex * 3), 3).setUsage(
        THREE.DynamicDrawUsage
      )
    );
    this.setAttribute(
      'normal',
      new THREE.BufferAttribute(new Float32Array(maxVertex * 3), 3).setUsage(
        THREE.DynamicDrawUsage
      )
    );
    this.setAttribute(
      'uv',
      new THREE.BufferAttribute(new Float32Array(maxVertex * 2), 2).setUsage(
        THREE.DynamicDrawUsage
      )
    );
    if (generateUv2) {
      this.setAttribute(
        'uv2',
        new THREE.BufferAttribute(new Float32Array(maxVertex * 2), 2).setUsage(
          THREE.DynamicDrawUsage
        )
      );
    }

    this.drawRange.start = 0;
    this.drawRange.count = 0;

    this.setIndex(new Array(maxVertex * 3));
  }

  _initByData(pathPointList, options = {}, usage, generateUv2) {
    const vertexData = generatePathVertexData(
      pathPointList,
      options,
      generateUv2
    );

    if (vertexData && vertexData.count !== 0) {
      this.setAttribute(
        'position',
        new THREE.BufferAttribute(
          new Float32Array(vertexData.position),
          3
        ).setUsage(usage || THREE.StaticDrawUsage)
      );
      this.setAttribute(
        'normal',
        new THREE.BufferAttribute(
          new Float32Array(vertexData.normal),
          3
        ).setUsage(usage || THREE.StaticDrawUsage)
      );
      this.setAttribute(
        'uv',
        new THREE.BufferAttribute(new Float32Array(vertexData.uv), 2).setUsage(
          usage || THREE.StaticDrawUsage
        )
      );
      if (generateUv2) {
        this.setAttribute(
          'uv2',
          new THREE.BufferAttribute(
            new Float32Array(vertexData.uv2),
            2
          ).setUsage(usage || THREE.StaticDrawUsage)
        );
      }

      this.setIndex(vertexData.indices);
    } else {
      this._initByMaxVertex(2, generateUv2);
    }
  }

  /**
   * Update geometry by PathPointList instance
   * @param {PathPointList} pathPointList
   * @param {Object} options
   * @param {Number} [options.width=0.1]
   * @param {Number} [options.progress=1]
   * @param {Boolean} [options.arrow=true]
   * @param {String} [options.side='both'] - "left"/"right"/"both"
   * @param callback
   */
  update(pathPointList, options = {}, callback) {
    const generateUv2 = !!this.getAttribute('uv2');

    const vertexData = generatePathVertexData(
      pathPointList,
      options,
      generateUv2
    );

    callback &&
      callback([
        vertexData.position[vertexData.position.length - 3],
        vertexData.position[vertexData.position.length - 2],
        vertexData.position[vertexData.position.length - 1]
      ]);
    if (vertexData) {
      this._updateAttributes(
        vertexData.position,
        vertexData.normal,
        vertexData.uv,
        generateUv2 ? vertexData.uv2 : null,
        vertexData.indices
      );
      this.drawRange.count = vertexData.count;
    } else {
      this.drawRange.count = 0;
    }
  }

  _resizeAttribute(name, len) {
    let attribute = this.getAttribute(name);
    while (attribute.array.length < len) {
      const oldLength = attribute.array.length;
      const newAttribute = new THREE.BufferAttribute(
        new Float32Array(oldLength * 2),
        attribute.itemSize,
        attribute.normalized
      );
      newAttribute.name = attribute.name;
      newAttribute.usage = attribute.usage;
      this.setAttribute(name, newAttribute);
      attribute = newAttribute;
    }
  }

  _resizeIndex(len) {
    let index = this.getIndex();
    while (index.array.length < len) {
      const oldLength = index.array.length;
      const newIndex = new THREE.BufferAttribute(
        oldLength * 2 > 65535
          ? new Uint32Array(oldLength * 2)
          : new Uint16Array(oldLength * 2),
        1
      );
      newIndex.name = index.name;
      newIndex.usage = index.usage;
      this.setIndex(newIndex);
      index = newIndex;
    }
  }

  _updateAttributes(position, normal, uv, uv2, indices) {
    this._resizeAttribute('position', position.length);
    const positionAttribute = this.getAttribute('position');
    positionAttribute.array.set(position, 0);
    positionAttribute.updateRange.count = position.length;
    positionAttribute.needsUpdate = true;

    this._resizeAttribute('normal', normal.length);
    const normalAttribute = this.getAttribute('normal');
    normalAttribute.array.set(normal, 0);
    normalAttribute.updateRange.count = normal.length;
    normalAttribute.needsUpdate = true;

    this._resizeAttribute('uv', uv.length);
    const uvAttribute = this.getAttribute('uv');
    uvAttribute.array.set(uv, 0);
    uvAttribute.updateRange.count = uv.length;
    uvAttribute.needsUpdate = true;

    if (uv2) {
      this._resizeAttribute('uv2', uv2.length);
      const uv2Attribute = this.getAttribute('uv2');
      uv2Attribute.array.set(uv2, 0);
      uv2Attribute.updateRange.count = uv2.length;
      uv2Attribute.needsUpdate = true;
    }

    this._resizeIndex(indices.length);
    const indexAttribute = this.getIndex();
    indexAttribute.set(indices, 0);
    indexAttribute.updateRange.count = indices.length;
    indexAttribute.needsUpdate = true;
  }
}

// Vertex Data Generate Functions

function generatePathVertexData(pathPointList, options, generateUv2 = false) {
  const width = options.width || 0.1;
  const progress = options.progress !== undefined ? options.progress : 1;
  const arrow = options.arrow !== undefined ? options.arrow : true;
  const side = options.side !== undefined ? options.side : 'both';

  const halfWidth = width / 2;
  const sideWidth = side !== 'both' ? width / 2 : width;
  const totalDistance = pathPointList.distance();
  const progressDistance = progress * totalDistance;
  if (totalDistance === 0) {
    return null;
  }

  const sharpUvOffset = halfWidth / sideWidth;
  const sharpUvOffset2 = halfWidth / totalDistance;

  let count = 0;

  // modify data
  const position = [];
  const normal = [];
  const uv = [];
  const uv2 = [];
  const indices = [];
  let verticesCount = 0;

  const right = new THREE.Vector3();
  const left = new THREE.Vector3();

  // for sharp corners
  const leftOffset = new THREE.Vector3();
  const rightOffset = new THREE.Vector3();
  const tempPoint1 = new THREE.Vector3();
  const tempPoint2 = new THREE.Vector3();

  function addVertices(pathPoint) {
    const first = position.length === 0;
    const sharpCorner = pathPoint.sharp && !first;

    const uvDist = pathPoint.dist / sideWidth;
    const uvDist2 = pathPoint.dist / totalDistance;

    const dir = pathPoint.dir;
    const up = pathPoint.up;
    const _right = pathPoint.right;

    if (side !== 'left') {
      right.copy(_right).multiplyScalar(halfWidth * pathPoint.widthScale);
    } else {
      right.set(0, 0, 0);
    }

    if (side !== 'right') {
      left.copy(_right).multiplyScalar(-halfWidth * pathPoint.widthScale);
    } else {
      left.set(0, 0, 0);
    }

    right.add(pathPoint.pos);
    left.add(pathPoint.pos);

    if (sharpCorner) {
      leftOffset.fromArray(position, position.length - 6).sub(left);
      rightOffset.fromArray(position, position.length - 3).sub(right);

      const leftDist = leftOffset.length();
      const rightDist = rightOffset.length();

      const sideOffset = leftDist - rightDist;
      let longerOffset, longEdge;

      if (sideOffset > 0) {
        longerOffset = leftOffset;
        longEdge = left;
      } else {
        longerOffset = rightOffset;
        longEdge = right;
      }

      tempPoint1
        .copy(longerOffset)
        .setLength(Math.abs(sideOffset))
        .add(longEdge);

      const _cos = tempPoint2
        .copy(longEdge)
        .sub(tempPoint1)
        .normalize()
        .dot(dir);
      const _len = tempPoint2
        .copy(longEdge)
        .sub(tempPoint1)
        .length();
      const _dist = _cos * _len * 2;

      tempPoint2
        .copy(dir)
        .setLength(_dist)
        .add(tempPoint1);

      if (sideOffset > 0) {
        position.push(
          tempPoint1.x,
          tempPoint1.y,
          tempPoint1.z, // 6
          right.x,
          right.y,
          right.z, // 5
          left.x,
          left.y,
          left.z, // 4
          right.x,
          right.y,
          right.z, // 3
          tempPoint2.x,
          tempPoint2.y,
          tempPoint2.z, // 2
          right.x,
          right.y,
          right.z // 1
        );

        verticesCount += 6;

        indices.push(
          verticesCount - 6,
          verticesCount - 8,
          verticesCount - 7,
          verticesCount - 6,
          verticesCount - 7,
          verticesCount - 5,

          verticesCount - 4,
          verticesCount - 6,
          verticesCount - 5,
          verticesCount - 2,
          verticesCount - 4,
          verticesCount - 1
        );

        count += 12;
      } else {
        position.push(
          left.x,
          left.y,
          left.z, // 6
          tempPoint1.x,
          tempPoint1.y,
          tempPoint1.z, // 5
          left.x,
          left.y,
          left.z, // 4
          right.x,
          right.y,
          right.z, // 3
          left.x,
          left.y,
          left.z, // 2
          tempPoint2.x,
          tempPoint2.y,
          tempPoint2.z // 1
        );

        verticesCount += 6;

        indices.push(
          verticesCount - 6,
          verticesCount - 8,
          verticesCount - 7,
          verticesCount - 6,
          verticesCount - 7,
          verticesCount - 5,

          verticesCount - 6,
          verticesCount - 5,
          verticesCount - 3,
          verticesCount - 2,
          verticesCount - 3,
          verticesCount - 1
        );

        count += 12;
      }

      normal.push(
        up.x,
        up.y,
        up.z,
        up.x,
        up.y,
        up.z,
        up.x,
        up.y,
        up.z,
        up.x,
        up.y,
        up.z,
        up.x,
        up.y,
        up.z,
        up.x,
        up.y,
        up.z
      );

      uv.push(
        uvDist - sharpUvOffset,
        0,
        uvDist - sharpUvOffset,
        1,
        uvDist,
        0,
        uvDist,
        1,
        uvDist + sharpUvOffset,
        0,
        uvDist + sharpUvOffset,
        1
      );

      if (generateUv2) {
        uv2.push(
          uvDist2 - sharpUvOffset2,
          0,
          uvDist2 - sharpUvOffset2,
          1,
          uvDist2,
          0,
          uvDist2,
          1,
          uvDist2 + sharpUvOffset2,
          0,
          uvDist2 + sharpUvOffset2,
          1
        );
      }
    } else {
      position.push(left.x, left.y, left.z, right.x, right.y, right.z);

      normal.push(up.x, up.y, up.z, up.x, up.y, up.z);

      uv.push(uvDist, 0, uvDist, 1);

      if (generateUv2) {
        uv2.push(uvDist2, 0, uvDist2, 1);
      }

      verticesCount += 2;

      if (!first) {
        indices.push(
          verticesCount - 2,
          verticesCount - 4,
          verticesCount - 3,
          verticesCount - 2,
          verticesCount - 3,
          verticesCount - 1
        );

        count += 6;
      }
    }
  }

  const sharp = new THREE.Vector3();

  function addStart(pathPoint) {
    const dir = pathPoint.dir;
    const up = pathPoint.up;
    const _right = pathPoint.right;

    const uvDist = pathPoint.dist / sideWidth;
    const uvDist2 = pathPoint.dist / totalDistance;

    if (side !== 'left') {
      right.copy(_right).multiplyScalar(halfWidth * 2);
    } else {
      right.set(0, 0, 0);
    }

    if (side !== 'right') {
      left.copy(_right).multiplyScalar(-halfWidth * 2);
    } else {
      left.set(0, 0, 0);
    }

    sharp.copy(dir).setLength(halfWidth * 3);

    right.add(pathPoint.pos);
    left.add(pathPoint.pos);
    sharp.add(pathPoint.pos);

    position.push(
      left.x,
      left.y,
      left.z,
      right.x,
      right.y,
      right.z,
      sharp.x,
      sharp.y,
      sharp.z
    );

    normal.push(up.x, up.y, up.z, up.x, up.y, up.z, up.x, up.y, up.z);

    uv.push(
      uvDist,
      side !== 'both' ? (side !== 'right' ? -2 : 0) : -0.5,
      uvDist,
      side !== 'both' ? (side !== 'left' ? 2 : 0) : 1.5,
      uvDist + 1.5,
      side !== 'both' ? 0 : 0.5
    );

    if (generateUv2) {
      uv2.push(
        uvDist2,
        side !== 'both' ? (side !== 'right' ? -2 : 0) : -0.5,
        uvDist2,
        side !== 'both' ? (side !== 'left' ? 2 : 0) : 1.5,
        uvDist2 + (1.5 * width) / totalDistance,
        side !== 'both' ? 0 : 0.5
      );
    }

    verticesCount += 3;

    indices.push(verticesCount - 1, verticesCount - 3, verticesCount - 2);

    count += 3;
  }

  let lastPoint;

  if (progressDistance > 0) {
    for (let i = 0; i < pathPointList.count; i++) {
      const pathPoint = pathPointList.array[i];

      if (pathPoint.dist > progressDistance) {
        const prevPoint = pathPointList.array[i - 1];
        lastPoint = new PathPoint();

        // linear lerp for progress
        const alpha =
          (progressDistance - prevPoint.dist) /
          (pathPoint.dist - prevPoint.dist);
        lastPoint.lerpPathPoints(prevPoint, pathPoint, alpha);

        addVertices(lastPoint);
        break;
      } else {
        addVertices(pathPoint);
      }
    }
  } else {
    lastPoint = pathPointList.array[0];
  }

  // build arrow geometry
  if (arrow) {
    lastPoint = lastPoint || pathPointList.array[pathPointList.count - 1];
    addStart(lastPoint);
  }

  return {
    position,
    normal,
    uv,
    uv2,
    indices,
    count
  };
}

/**
 * PathTubeGeometry
 */
class PathTubeGeometry extends PathGeometry {
  /**
   * @param {Object|Number} initData - If initData is number, geometry init by empty data and set it as the max vertex. If initData is Object, it contains pathPointList and options.
   * @param {Boolean} [generateUv2=false]
   */
  constructor(initData = 1000, generateUv2 = false) {
    super(initData, generateUv2);
  }

  _initByData(pathPointList, options = {}, usage, generateUv2) {
    const vertexData = generateTubeVertexData(
      pathPointList,
      options,
      generateUv2
    );

    if (vertexData && vertexData.count !== 0) {
      this.setAttribute(
        'position',
        new THREE.BufferAttribute(
          new Float32Array(vertexData.position),
          3
        ).setUsage(usage || THREE.StaticDrawUsage)
      );
      this.setAttribute(
        'normal',
        new THREE.BufferAttribute(
          new Float32Array(vertexData.normal),
          3
        ).setUsage(usage || THREE.StaticDrawUsage)
      );
      this.setAttribute(
        'uv',
        new THREE.BufferAttribute(new Float32Array(vertexData.uv), 2).setUsage(
          usage || THREE.StaticDrawUsage
        )
      );
      if (generateUv2) {
        this.setAttribute(
          'uv2',
          new THREE.BufferAttribute(
            new Float32Array(vertexData.uv2),
            2
          ).setUsage(usage || THREE.StaticDrawUsage)
        );
      }

      this.setIndex(vertexData.indices);
    } else {
      this._initByMaxVertex(2, generateUv2);
    }
  }

  /**
   * Update geometry by PathPointList instance
   * @param {PathPointList} pathPointList
   * @param {Object} options
   * @param {Number} [options.radius=0.1]
   * @param {Number} [options.progress=1]
   * @param {Boolean} [options.radialSegments=8]
   * @param {String} [options.startRad=0]
   */
  update(pathPointList, options = {}) {
    const generateUv2 = !!this.getAttribute('uv2');

    const vertexData = generateTubeVertexData(
      pathPointList,
      options,
      generateUv2
    );

    if (vertexData) {
      this._updateAttributes(
        vertexData.position,
        vertexData.normal,
        vertexData.uv,
        generateUv2 ? vertexData.uv2 : null,
        vertexData.indices
      );
      this.drawRange.count = vertexData.count;
    } else {
      this.drawRange.count = 0;
    }
  }
}

// Vertex Data Generate Functions

function generateTubeVertexData(pathPointList, options, generateUv2 = false) {
  const radius = options.radius || 0.1;
  const progress = options.progress !== undefined ? options.progress : 1;
  const radialSegments = Math.max(2, options.radialSegments || 8);
  const startRad = options.startRad || 0;

  const circum = radius * 2 * Math.PI;
  const totalDistance = pathPointList.distance();
  const progressDistance = progress * totalDistance;
  if (progressDistance === 0) {
    return null;
  }

  let count = 0;

  // modify data
  const position = [];
  const normal = [];
  const uv = [];
  const uv2 = [];
  const indices = [];
  let verticesCount = 0;

  const normalDir = new THREE.Vector3();

  function addVertices(pathPoint, radius, radialSegments) {
    const first = position.length === 0;
    const uvDist = pathPoint.dist / circum;
    const uvDist2 = pathPoint.dist / totalDistance;

    for (let r = 0; r <= radialSegments; r++) {
      let _r = r;
      if (_r === radialSegments) {
        _r = 0;
      }
      normalDir
        .copy(pathPoint.up)
        .applyAxisAngle(
          pathPoint.dir,
          startRad + (Math.PI * 2 * _r) / radialSegments
        )
        .normalize();

      position.push(
        pathPoint.pos.x + normalDir.x * radius * pathPoint.widthScale,
        pathPoint.pos.y + normalDir.y * radius * pathPoint.widthScale,
        pathPoint.pos.z + normalDir.z * radius * pathPoint.widthScale
      );
      normal.push(normalDir.x, normalDir.y, normalDir.z);
      uv.push(uvDist, r / radialSegments);

      if (generateUv2) {
        uv2.push(uvDist2, r / radialSegments);
      }

      verticesCount++;
    }

    if (!first) {
      const begin1 = verticesCount - (radialSegments + 1) * 2;
      const begin2 = verticesCount - (radialSegments + 1);

      for (let i = 0; i < radialSegments; i++) {
        indices.push(
          begin2 + i,
          begin1 + i,
          begin1 + i + 1,
          begin2 + i,
          begin1 + i + 1,
          begin2 + i + 1
        );

        count += 6;
      }
    }
  }

  if (progressDistance > 0) {
    for (let i = 0; i < pathPointList.count; i++) {
      const pathPoint = pathPointList.array[i];

      if (pathPoint.dist > progressDistance) {
        const prevPoint = pathPointList.array[i - 1];
        const lastPoint = new PathPoint();

        // linear lerp for progress
        const alpha =
          (progressDistance - prevPoint.dist) /
          (pathPoint.dist - prevPoint.dist);
        lastPoint.lerpPathPoints(prevPoint, pathPoint, alpha);

        addVertices(lastPoint, radius, radialSegments);
        break;
      } else {
        addVertices(pathPoint, radius, radialSegments);
      }
    }
  }

  return {
    position,
    normal,
    uv,
    uv2,
    indices,
    count
  };
}

export const Path = { PathGeometry, PathPointList, PathTubeGeometry };
